package client;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.ListIterator;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;


import org.w3c.dom.Document;
import org.w3c.dom.Element;

import common.ClientObject;
import common.FileObject;
import common.JobObject;
import common.JobResultObject;
import common.JobsPackage;
import common.LoginRRObject;
import common.Util;

public class ClientService {

	private InetAddress masterIP;
	private int masterTCPPort;
	private String clientID;
	private int clientRevPort;
	private final static String MODEL = "com.itdcl.model";

	public ClientService () {
	}
	
	public ClientService (InetAddress masterIP, int masterTCPPort, String clientID) {
		this.masterIP = masterIP;
		this.masterTCPPort = masterTCPPort;
		this.clientID = clientID;
		}
	public void setClientRevPort(int clientRevPort) {
		this.clientRevPort = clientRevPort;
	}
    public void setClientID(String aClientID){
        this.clientID = aClientID;
    }

    public String getClientID(){
        return clientID;
    }
	
	public String login() {
		
		Socket s = null;
		try{
		s = new Socket(masterIP, masterTCPPort);
		System.out.println("SYSINFO - Connect to server");
		
		DataOutputStream dataOut = new DataOutputStream(s.getOutputStream());
		DataInputStream dataIn = new DataInputStream(s.getInputStream());
		
		String loginRequestXML = createLoginXML();
		System.out.println("DEBUG - XML Request:");
		System.out.println(loginRequestXML);
		dataOut.writeUTF(loginRequestXML);
		System.out.println("SYSINFO - Send Login XML.");
		
		String loginReplyXML = dataIn.readUTF();
		System.out.println("DEBUG - loginReplyXML received:\n" + loginReplyXML);
		
		JAXBContext context = JAXBContext.newInstance(LoginRRObject.class);
		Unmarshaller u = context.createUnmarshaller();
		StringBuffer xmlStr = new StringBuffer(loginReplyXML);
		LoginRRObject loginRRObject = (LoginRRObject) u.unmarshal(new StreamSource(new StringReader(xmlStr.toString())));
		
		this.clientID = loginRRObject.getClient().getClientID();
		System.out.println("DEBUG - ClientID:" + this.clientID);
		
		}catch (Exception e) {return "error";}
	     finally {
		  if(s!=null) {
			try {
				s.close();
				System.out.println("SYSINFO - Client Login to Master TCP connection is closed.");
			} catch(IOException e){}
		  }
	     }
	     return this.clientID;
	}
	     
	public void sendJobs(JobsPackage jp){

		Socket s = null;
		try {
			s = new Socket(masterIP, masterTCPPort);

			DataOutputStream dataOut = new DataOutputStream(s.getOutputStream());
			
			JAXBContext context = JAXBContext.newInstance(JobsPackage.class);
		    Marshaller m = context.createMarshaller();
		    m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

		    jp.setFrom("Client");
		    jp.setType("SendJobs");
			jp.setClientID(clientID);
		    
		    ByteArrayOutputStream baos = new ByteArrayOutputStream();
		    m.marshal(jp, baos);
		    String jobsXML = baos.toString();
			
			System.out.println("DEBUG - XML Request:");
			System.out.println(jobsXML);

			dataOut.writeUTF(jobsXML);
			System.out.println("SYSINFO - Send a Job Package Information.");
			
			ListIterator<JobObject> iter = jp.getJobs().listIterator();
			while (iter.hasNext()) {
				JobObject job = iter.next();
				String filePath = job.getExecuteFilePath() +"/"+ job.getExecuteFileName() + ".class";
				File file = new File(filePath);
				dataOut.writeLong(file.length());
			}
	    	
			 iter = jp.getJobs().listIterator();
			
	    	while (iter.hasNext()) {
	    		JobObject job = iter.next();

	    		System.out.println("DEBUG - fileName = " + job.getExecuteFileName());
	    		System.out.println("DEBUG - parameters = " + job.getParameters());
	    		System.out.println("DEBUG - time = " + job.getTime());
	    		System.out.println("DEBUG - jobID = " + job.getJobID());
	    		System.out.println("DEBUG - priority = " + job.getPriority());
	    		
	    		String filePath = job.getExecuteFilePath()+"/"+job.getExecuteFileName()+".class";
	            File file = new File(filePath);
	            System.out.println("DEBUG - File Path: " + filePath);
	            System.out.println("DEBUG - File length: " + (int) file.length());
	            
	            // send the file content
	            DataInputStream fileIn = new DataInputStream(new BufferedInputStream(new FileInputStream(filePath)));
	            int bufferSize = 8192;
	            byte[] buf = new byte[bufferSize];

	            int length = 0;
	            while((length = fileIn.read(buf))!=-1) {
	            	dataOut.write(buf, 0, length);
	            }
	            dataOut.flush();
	            System.out.println("SYSINFO - Send a job file.");
	    	}
	    	System.out.println("SYSINFO - Send a Job Package.");
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (JAXBException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			if(s!=null) {
				try {
					s.close();
					System.out.println("SYSINFO - Client send jobs to Master TCP connection is closed.");
				} catch(IOException e){}
			}
		}
	}
	

	
	public JobsPackage QueryJobsSocket(String aClientID)
	{
		Socket s = null;
		JobsPackage jobsPackage = new JobsPackage();
		try{
		s = new Socket(masterIP, masterTCPPort);
		System.out.println("SYSINFO - Connect to server");
		
		DataOutputStream dataOut = new DataOutputStream(s.getOutputStream());
		DataInputStream dataIn = new DataInputStream(s.getInputStream());
		
		String queryXML = createQueryXML();
		dataOut.writeUTF(queryXML);
		System.out.println("SYSINFO - Send Query XML.");
		
		String queryReplyXML = dataIn.readUTF();
	    System.out.println("DEBUG - queryReplyXML received:\n" + queryReplyXML);
	    JAXBContext context = JAXBContext.newInstance(JobsPackage.class);
		Unmarshaller u = context.createUnmarshaller();
		StringBuffer xmlStr = new StringBuffer(queryReplyXML);
		jobsPackage = (JobsPackage) u.unmarshal(new StreamSource(new StringReader(xmlStr.toString())));
		}catch (Exception e) {e.printStackTrace();}
	     finally {
		  if(s!=null) {
			try {
				s.close();
				System.out.println("SYSINFO - Client Query to Master TCP connection is close.");
			} catch(IOException e){}
		  }
	     }
	     return jobsPackage;
	}
	
	
	public String KillJobSocket(String aClientID,int aJobID)
	{
		Socket s = null;
		JobsPackage jobsPackage = new JobsPackage();
		String killJobReply = null;
		try{
		s = new Socket(masterIP, masterTCPPort);
		System.out.println("SYSINFO - Connect to server");
		
		DataOutputStream dataOut = new DataOutputStream(s.getOutputStream());
		DataInputStream dataIn = new DataInputStream(s.getInputStream());
		
		String killJobXML = createKillJobXML(aJobID);
		dataOut.writeUTF(killJobXML);
		System.out.println("SYSINFO - Send Kill Job XML.");
			
	    //killJobReply = dataIn.readUTF();
		
	    System.out.println("DEBUG - killJobReply received:\n" + killJobReply);
		
		}catch (Exception e) {e.printStackTrace();}
	     finally {
		  if(s!=null) {
			try {
				s.close();
				System.out.println("SYSINFO - Client Query to Server TCP connection is close.");
			} catch(IOException e){}
		  }
	     }
	     return killJobReply;
	}
	
	public void receiveResults(String savePath) {

		System.out.println("SYSINFO - Waiting job result from master.");
		Socket masterSocket = null;
		try {
			masterSocket = new Socket(masterIP, masterTCPPort);
			InputStream in = masterSocket.getInputStream();
			DataInputStream dataIn = new DataInputStream(in);
			OutputStream out = masterSocket.getOutputStream();
			DataOutputStream dataOut = new DataOutputStream(out);
			
			String queryXML = createRetrieveOutputXML();
			dataOut.writeUTF(queryXML);
			System.out.println("SYSINFO - Send Query XML.");

			String jobResultXML = dataIn.readUTF();
			System.out.println("SYSINFO - Received a job result.");
			System.out.println("DEBUG - result XML: " + jobResultXML);

			/********** JobResult Information Transfer **********/
			JAXBContext context = JAXBContext.newInstance(JobResultObject.class);
			Unmarshaller u = context.createUnmarshaller();
			StringBuffer xmlStr = new StringBuffer(jobResultXML);
			JobResultObject jobResult = (JobResultObject) u.unmarshal(new StreamSource(new StringReader(xmlStr.toString())));

			ArrayList<FileObject> fileList = new ArrayList<FileObject>();
			if (jobResult.getFrom().equals("Master") && jobResult.getType().equals("SendJobResult")) {

				fileList.addAll(jobResult.getFiles());

				String filePath = "clientJobs"; // default jobs folder (but can be assign to another one)
				System.out.println(filePath);
				File file = new File(filePath);
				if (file.exists()==false) {
					file.mkdirs();
				}

				/********** File Transfer **********/
				savePath += filePath;
				File tempFile = new File(savePath);
				if (tempFile.exists()==false) {
					tempFile.mkdirs();
				}
				int bufferSize = 8192;
				byte[] buf = new byte[bufferSize];

				Iterator fileIter = fileList.iterator();
				FileObject fileObject = new FileObject();

				String tempPath;
				int length = 0;
				while ((length = dataIn.read(buf)) != -1) {
					System.out.println("DEBUG - Read data from connection into buffer.");

					int bufRead = 0;

					while (bufRead != bufferSize && fileIter.hasNext()) {
						System.out.println("DEBUG - Read data from buffer.");
						if (fileObject.isCompleted() && fileIter.hasNext()){
							System.out.println("DEBUG - A new File.");
							fileObject = (FileObject) fileIter.next();
						}
						if (fileObject.getFileName().endsWith(".class")) {
							System.out.println("DEBUG - Skip Sending Class file.");
							fileObject.setCompleted(true);
							continue;
						}
						if (fileObject.getFileSize() == 0) {
							System.out.println("DEBUG - File size is 0.");
							System.out.println("SYSINFO - File saved at " + fileObject.getFileName());
							tempPath = savePath + "/"+ fileObject.getFileName();
							DataOutputStream fileOut = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(tempPath)));
							fileOut.write(buf, bufRead, (int) fileObject.getFileSize());
							fileOut.close();
							fileObject.setCompleted(true);
							continue;
						}
						long befLeftLong = fileObject.getFileSize() - fileObject.getFileBufSave();
						int befLeft = (int) befLeftLong; // not safe casting

						if (bufferSize - bufRead >= befLeft && befLeft != 0) {
							System.out.println("DEBUG - Completed in one write operation. " + fileObject.getFileName());
							tempPath = savePath + "/" + fileObject.getFileName();
							DataOutputStream fileOut = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(tempPath)));
							fileOut.write(buf, bufRead, (int) fileObject.getFileSize());
							fileOut.close();
							System.out.println("SYSINFO - File saved at " + fileObject.getFileName());
							fileObject.setFileBufSave(fileObject.getFileSize());
							bufRead += fileObject.getFileSize();
							fileObject.setCompleted(true);

						} else if (bufferSize - bufRead < befLeft) { // NTD: handle
							tempPath = savePath + "/" + fileObject.getFileName();
							DataOutputStream fileOut = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(tempPath)));
							fileOut.write(buf, bufRead, bufferSize);
							System.out.println("DEBUG - File transfer haven't completed");
							fileObject.setFileBufSave(bufferSize - bufRead);
							fileOut.close(); // file has been written out, Master may scheduler an uncompleted file
							bufRead = bufferSize;
							fileObject.setCompleted(false);

						}
					}

				}
				
			} else {
				System.out.println("SYSINFO - The reply is not a Job Result. No action will be performed.");
			}

			masterSocket.close();
			System.out.println("SYSINFO - Client TCP connection is close.");
		} catch (EOFException e) {
			System.out.println("EOF:" + e.getMessage());
		} catch (IOException e) {
			System.out.println("IO:" + e.getMessage());
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if (masterSocket != null)
					masterSocket.close();
			} catch (IOException e) {/* close failed */
			}
		}
	}
	
	
	
	// create XML document for login
	public String createLoginXML() throws Exception {
		
		JAXBContext context = JAXBContext.newInstance(LoginRRObject.class);
		Marshaller m = context.createMarshaller();
	    m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
	    
	    LoginRRObject loginRRObject = new LoginRRObject();
	    loginRRObject.setFrom("Client");
	    loginRRObject.setType("Login");
	    loginRRObject.setTimestamp(getTime());
	    
	    ClientObject client = new ClientObject();
	    client.setClientIP(InetAddress.getLocalHost().getHostAddress());
	    client.setClientRevPort(clientRevPort);
	    
	    loginRRObject.setClient(client);
	    
	    ByteArrayOutputStream baos = new ByteArrayOutputStream();
	    m.marshal(loginRRObject, baos);
	    String loginRequestXML = baos.toString();
	    
		return loginRequestXML;
		
    }
	
	// create XML document for query
	public String createQueryXML() throws Exception {
		DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder db = null;
		//jp.setClientID(ClientID);
		try {
			db = dbfactory.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	    System.out.println("DEBUG - ClientID: " + clientID);
		
	    Document outDoc = db.newDocument();
		outDoc.setXmlVersion("1.0");
		Element outRoot = outDoc.createElement("Request");
		outRoot.setAttribute("type", "QueryJobs");
		outRoot.setAttribute("from", "Client");
		outRoot.setAttribute("version", "1");
		
		Element clientIDElement = outDoc.createElement("clientID");
		clientIDElement.appendChild(outDoc.createTextNode(clientID));
		outRoot.appendChild(clientIDElement);
		
	    outDoc.appendChild(outRoot);
	
	    String xmlStr = Util.docToString(outDoc);
	    System.out.println("DEBUG - XML Query: "+xmlStr);

	    return xmlStr;
    }
	
	
	// create XML document for kill job
	public String createKillJobXML(int aJobID) throws Exception {
		DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder db = null;
		//jp.setClientID(ClientID);
		try {
			db = dbfactory.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		Document outDoc = db.newDocument();
		outDoc.setXmlVersion("1.0");
		Element outRoot = outDoc.createElement("Request");
		outRoot.setAttribute("type", "KillJob");
		outRoot.setAttribute("from", "Client");
		outRoot.setAttribute("version", "1");
		
		Element clientIDElement = outDoc.createElement("clientID");
		clientIDElement.appendChild(outDoc.createTextNode(clientID));
		outRoot.appendChild(clientIDElement);
		
		Element jobIDElement = outDoc.createElement("jobID");
		jobIDElement.appendChild(outDoc.createTextNode(Integer.toString(aJobID)));
		outRoot.appendChild(jobIDElement);
		
	    outDoc.appendChild(outRoot);
	
	    String xmlStr = Util.docToString(outDoc);
	    System.out.println("DEBUG - XML Kill Job: "+xmlStr);

	    return xmlStr;
    }
	

	// create XML document for retriveing output
	public String createRetrieveOutputXML() throws Exception {
		DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder db = null;
		//jp.setClientID(ClientID);
		try {
			db = dbfactory.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		Document outDoc = db.newDocument();
		outDoc.setXmlVersion("1.0");
		Element outRoot = outDoc.createElement("Request");
		outRoot.setAttribute("type", "RetrieveOutput");
		outRoot.setAttribute("from", "Client");
		outRoot.setAttribute("version", "1");
		
		Element clientIDElement = outDoc.createElement("clientID");
		clientIDElement.appendChild(outDoc.createTextNode(clientID));
		outRoot.appendChild(clientIDElement);
		
	    outDoc.appendChild(outRoot);
	
	    String xmlStr = Util.docToString(outDoc);
	    System.out.println("DEBUG - XML Retrieve Output: "+xmlStr);

	    return xmlStr;
    }
	
	public Long getTime() {
		Date date = new Date();
		return date.getTime();
	}
}


